Skip to content

fix(session-flow): carry amendment bullets below the predecessor's Opening ask - #4164

Merged
kyle-sexton merged 3 commits into
mainfrom
fix/session-flow-save-point-amendment-carry
Sep 15, 2026
Merged

kyle-sexton merged 3 commits into
mainfrom
fix/session-flow-save-point-amendment-carry

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

save_point.py new dropped the predecessor's "Original goal" amendment bullets that sat below its
Opening ask: line, so every successor handoff silently lost them. This makes the section carry keep
every bullet in the predecessor's Original goal regardless of position, with one regression test that
fails on main and passes here.

No linked issue.

Root cause

plugins/session-flow/scripts/save_point.py:1093-1099 (pre-fix line numbers) — the new command
assembles the successor's ## Original goal by copying the predecessor's section and skipping the
verbatim opening ask:

if skip:
    if line.startswith("**"):
        skip = False
    else:
        continue
if line.startswith("Opening ask:") or line.startswith("**Next action serves it by:**"):
    skip = True
    continue

The skip is entered on Opening ask: and ends only on a line starting with **. An amendment
bullet starts with - , so every - **Amended (verbatim, …)** … bullet sitting BELOW the
predecessor's Opening ask: line is swallowed and never reaches the successor. The **-only
terminator was deliberate (added in #3718) so that paragraph 2 of a multi-paragraph verbatim ask
would not be smuggled into the successor's goal block; it is simply too narrow.

Observed twice on a real chain, both times repaired by hand:

  • .work/handoffs/20260908T151602Z-handoff-plugin-sync-3688-item4-followups-merged.md — carries
    the note (The two hop-9 bullets above were re-added this hop verbatim from the hop-9 file on disk; save_point.py new did not carry them because they sat below that file's Opening ask: line.)
  • .work/handoffs/20260908T173036Z-handoff-plugin-sync-3688-all-followups-closed.md — same, three
    bullets re-added from the hop-10 file.

Fix

One predicate, reusing the module's existing BULLET_RE:

def _ends_opening_ask(line: str) -> bool:
    return line.startswith("**") or bool(BULLET_RE.match(line))

The skip now ends at any structural marker — a ** line or a bullet. A verbatim opening ask
carries no bullets by contract (the skeleton's own opening-ask slot says "no bullets"), so
multi-paragraph asks are still excluded exactly as before; only bullets, which are never ask
content, now end the skip.

All three reads of the section route through the predicate: the carry in build_skeleton, the
goal-quote skip in _check_original_goal, and (after review feedback) that function's hop-1
OPENING_ASK_CAP counter — one definition of the section boundary rather than three that can drift.

Carried bullets land above the Opening ask: see … § Original goal pointer, which is where an
amendment belongs; the new test pins that too.

Verification

One case added to the pytest suite that plugins/session-flow/scripts/save_point.test.sh execs
(tests/test_save_point.py::test_new_hop2_carries_amendments_below_the_opening_ask). It builds a
hop-1 save-point, appends one amendment bullet below its Opening ask: line, runs new --previous,
and asserts the bullet survives into hop 2 above the pointer and that hop 2 still validates strict.

Fails on current main's save_point.py (test present, fix stashed out):

E       assert '- **Amended (verbatim, 2026-09-01):** "Also do the other thing."' in '---\ntype: handoff\n…'
tests\test_save_point.py:657: AssertionError
FAILED tests/test_save_point.py::test_new_hop2_carries_amendments_below_the_opening_ask
1 failed, 72 deselected in 0.28s

Passes with the fix, and the whole file stays green:

1 passed, 72 deselected in 0.38s
73 passed in 8.81s   (73 passed after the review follow-up commit as well)

Also run locally: scripts/affected-tests.sh (selects save_point.test.sh,
tests/test_save_point.py, harness/hop_chain.test.sh), harness/hop_chain.test.sh (22/22 dry-run
cases + the 20-hop budget projection, PASS), scripts/validate-plugins.sh (all manifests + catalog),
scripts/run-ruff.sh check on both edited .py files (clean).

Version

Patch bump 0.35.12 → 0.35.13 plus a ### Fixed CHANGELOG top-insert. .claude-plugin/marketplace.json
carries no version field for session-flow, so there is no catalog line to bump (the only other
0.35.12 hit in the repo is the work-items plugin's own unrelated entry).

Not verified

  • The Windows host has no pytest, so save_point.test.sh SKIPs here by design; the suite was run
    through an ephemeral uv run --with pytest environment instead. The Linux CI lane is the truth
    for the runner path itself.
  • affected-tests.sh --run is a Linux-only gate and was not run; the three selected suites were run
    individually instead.
  • The hop-1 OPENING_ASK_CAP warn path has no test coverage before or after this change; the
    one-line consistency fix there is reasoned, not pinned.

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_011mbb3nkTfMrT9vDmoUnQuj

@kyle-sexton
kyle-sexton marked this pull request as ready for review September 14, 2026 14:43
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-14T14:46:46.634032Z e0d963d Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 53s —— View job


Security review — /review:security-review

  • Gather context (PR diff via gh pr diff 4164, read changed files)
  • Apply skip gate
  • Report result

Skip gate hit: no security-relevant surface.

This PR's entire diff is:

  • plugins/session-flow/scripts/save_point.py — lines 340–346 add a helper _ends_opening_ask() reusing the existing BULLET_RE (a bounded, non-backtracking regex — no ReDoS concern), and lines 590–596 / lines 1098–1104 swap it in for the old line.startswith("**") terminator in two markdown section-skip loops.
  • plugins/session-flow/scripts/tests/test_save_point.py — one new pytest case covering the fix.
  • plugins/session-flow/CHANGELOG.md / .claude-plugin/plugin.json — version bump and changelog entry only.

This is a local text-parsing change to how the save_point.py new command delimits a markdown section while assembling one locally-authored handoff file from another (both produced and consumed by the same toolchain, not external/untrusted input). There's no authentication, authorization, secret handling, command execution, path construction, or trust-boundary crossing anywhere in the diff — just where a copy loop stops skipping lines. No CI/Actions workflow files are touched.

No security findings. Per the skill's high-signal bar, nothing further is reported.

@github-actions

Copy link
Copy Markdown
Contributor

Last security-reviewed head: e0d963d86371f09beafc7c436b2924b2d54e1b3c. On the next push, the relevance gate compares only the commits since this SHA; delete this comment to force a full re-review.

@claude

claude Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 3m 27s —— View job


Code review (/review:code-review)

Reviewed the diff against origin/main (4 files, +42/-3). This is a real, well-scoped bug fix with a reproducing regression test; the fix itself (_ends_opening_ask, reused at both call sites) is sound and matches the stated intent of collapsing two boundary definitions into one.

Finding

The fix leaves a third, still-inconsistent definition of the same boundary in _check_original_goal's OPENING_ASK_CAP counter.

plugins/session-flow/scripts/save_point.py#L607-L620

The PR's stated goal is "one definition of the section boundary rather than two that can drift," and it achieves that between the goal_lines skip (line 593) and build_skeleton's kept skip (line 1103) by routing both through _ends_opening_ask. But the hop-1 ask-length counter three lines below still uses the old, narrower boundary directly:

for line in body[ask_idx + 1 :]:
    if line.startswith("**"):
        break
    if line.strip():
        block += 1

Before this PR, that was consistent with the (also **-only) skip it sits next to. After this PR, it is not: if a hop-1 file is amended in place — the exact scenario this PR fixes the carry for, an amendment bullet added directly below hop-1's own Opening ask: line — and someone runs validate <hop1-file> --strict-transcript before hop 2 exists, this loop keeps counting past the bullet (since it only breaks on **) and folds the amendment's line(s) into the reported "ask length," while the goal_lines skip three lines above now correctly excludes that same bullet from the goal quote. The two loops in the same function disagree about where the ask ends.

Impact is soft — f.warn, not f.fail, so it can't fail --strict-transcript — but it can produce a misleading Original goal: 'Opening ask:' runs N lines (cap 15) warning that blames the verbatim ask for length actually contributed by an amendment bullet. Since _ends_opening_ask already exists and is exported for exactly this purpose, the natural fix is if _ends_opening_ask(line): break here too. The new test doesn't exercise this path: it only validates hop 2 (whose Opening ask: line is a see … pointer, so hop == 1 never triggers on it) and never runs validate on the amended hop-1 file directly.

Fix this →

Everything else checked, no issues

  • _ends_opening_ask's two call sites (save_point.py:593 and save_point.py:1103) are byte-consistent with the PR's claim, verified by reading both in full.
  • BULLET_RE (^(?:[-*+]|\d+[.)])\s+) correctly matches the amendment bullet's - **Amended (verbatim, …)** form, and doesn't collide with the pre-existing **Amended:** (non-bulleted) skeleton marker handling a few lines below.
  • Version bump and CHANGELOG.md entry are consistent with the repo's convention; plugin.json carries no other stale reference.
  • Author-claimed verification (pytest run, affected-tests.sh, hop_chain.test.sh, validate-plugins.sh, run-ruff.sh) is described in the PR body but not independently re-run here — noting per lane convention that it's author-claimed, not confirmed by this review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0d963d863

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/session-flow/scripts/save_point.py
@github-actions

Copy link
Copy Markdown
Contributor

Claude has reviewed this PR 1 time. The lane skips further automatic reviews after 5; deleting this comment resets the count.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

PR body contract — issue linkage

This PR body conforms to the issue-linkage contract. Nothing to do.

kyle-sexton added a commit that referenced this pull request Sep 14, 2026
The cap counter inside `_check_original_goal` still broke on `**` only, so
after the carry fix the same function held two disagreeing definitions of where
the opening ask ends: an amendment bullet below a hop-1 file's own `Opening
ask:` line was excluded from the goal quote but still counted against the ask
length, which can warn about ask length the ask did not contribute. It now uses
`_ends_opening_ask` like the other two reads.

Raised by the Claude review lane on PR #4164.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mbb3nkTfMrT9vDmoUnQuj
@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Review-lane follow-up: taken. The hop-1 OPENING_ASK_CAP counter in _check_original_goal now breaks on _ends_opening_ask(line) too (619cd0d), so all three reads of the section share one boundary instead of two agreeing and one drifting. The CHANGELOG entry says so. No test added for that path: the cap warn has no coverage before or after this change, and the brief for this PR is one regression case; the PR body records it as reasoned, not pinned.

The Codex P2 thread on the bullet terminator is answered inline and resolved: a narrower - **Amended match would still drop the non-bold parenthetical note bullets that appear in the real reproducer files, and over-carrying visible ask prose in a contract-violating case beats silently deleting operator-stated amendments.

kyle-sexton and others added 3 commits September 14, 2026 22:56
…ening ask

`save_point.py new` built the successor's Original goal by copying the
predecessor's section and skipping the verbatim opening ask. The skip entered
on `Opening ask:` and only ended on a line starting with `**`, so an amendment
bullet (`- **Amended (verbatim, ...)`) sitting below the ask was swallowed and
never reached the successor. Two hops of a real chain had to re-add those
bullets by hand.

The skip now ends at any structural marker: a `**` line or a bullet. A verbatim
ask carries no bullets by contract, so this keeps multi-paragraph asks out of
the successor, which is what the `**`-only terminator was for. The validator
reads the same section with the same loop, so both route through one
`_ends_opening_ask` predicate reusing the existing `BULLET_RE`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mbb3nkTfMrT9vDmoUnQuj
The cap counter inside `_check_original_goal` still broke on `**` only, so
after the carry fix the same function held two disagreeing definitions of where
the opening ask ends: an amendment bullet below a hop-1 file's own `Opening
ask:` line was excluded from the goal quote but still counted against the ask
length, which can warn about ask length the ask did not contribute. It now uses
`_ends_opening_ask` like the other two reads.

Raised by the Claude review lane on PR #4164.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mbb3nkTfMrT9vDmoUnQuj
The dropped bullets were re-added by hand at two hops of one handoff chain, not
across two chains.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mbb3nkTfMrT9vDmoUnQuj
@kyle-sexton
kyle-sexton force-pushed the fix/session-flow-save-point-amendment-carry branch from edc2034 to efd84b5 Compare September 15, 2026 02:59
@kyle-sexton
kyle-sexton merged commit a7e5f96 into main Sep 15, 2026
12 checks passed
@kyle-sexton
kyle-sexton deleted the fix/session-flow-save-point-amendment-carry branch September 15, 2026 03:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant